#include <vector>
#include <algorithm>
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>
#include "Receiver.h"
#include "ReceiverError.h"
#include "../../comm/tcp/TCPCommon.h"
#include "../../config/Config.h"
#include "../../system/Observers.h"

#define foreach BOOST_FOREACH

namespace kc_sf
{
	const std::string Wsp = "\t \r\n";
}

using namespace kc_sf;

// ---------------------------------------------------------------- //

Receiver::Receiver( const Connection &connection )
: CommRequestHandler( connection ),
  r_state( InitState ),
  r_comm( NULL )
{
}

// ---------------------------------------------------------------- //

Receiver::Receiver( CommCommon *comm, const Connection &connection )
: CommRequestHandler( connection ),
  r_state( InitState )
{
	if( comm == NULL )
		throw ReceiverError( NoConnectionWithClient,
				"SSLReceiver: Obiekt do komunikacji w konstruktorze to NULL" );

	r_comm = comm;
}

// ---------------------------------------------------------------- //

Receiver::~Receiver()
{
	if( r_comm )
		delete r_comm;
}

// ---------------------------------------------------------------- //

CommRequestHandler *Receiver::getInstance( const Connection &connection ) const
{
	return new Receiver( connection );
}

// ---------------------------------------------------------------- //

void Receiver::operator ()()
{
    if( sendReply( const_cast<const Reply &> ( StandardReplies.at( Ok_Greet ) ) )
    	!= Success ) // przywitaj sie z klientem
    {
        r_logger << error << receiver << "Nie udało się przesłać klientowi powitania" << flush;
        return;
    }

    r_state = ConnectedState;

    // odbieraj i przetwarzaj komendy
	while( r_state != InitState )
	{
		CommResult result = Success;
		Command cmd;
		if( ( result = recvCommand( cmd ) ) != Success )
			break;

	    switch( cmd.Type )
	    {
			case Quit:
				processQuit();
				return;

	        case Helo:
	            result = processHelo( cmd );
	            break;

	        case Ehlo:
	            result = processEhlo( cmd );
	            break;

	        case Mail:
	            result = processMailFrom( cmd );
	            break;

	        case Rcpt:
	            result = processRcpt( cmd );
	            break;

	        case Data:
	            result = processData();
	            break;

	        case Noop:
	            result = processNoop();
	            break;

	        case Rset:
	            result = processRset();
	            break;

	        case AuthPlain:
	        	result = processAuthPlain();
	        	break;

	        case AuthLogin:
	        	result = processAuthLogin();
	        	break;

	        case StartTLS:
	        	result = processStartTLS();
	        	break;

	        case Vrfy:
	        case Expn:
	        case Help:
	            result = processUnimplemented( cmd );
	            break;

	        case UndefinedCommand:
	        default:
	            result = processUndefined( cmd );
	            break;
	    }

	    if( result == Failure )
	    {
	    	sendReply( const_cast<const Reply &>( StandardReplies.at( Tmp_Aborted ) ) );
	    	break;
	    }
	    else if( result == Timeout )
	    {
	    	sendReply( const_cast<const Reply &>( StandardReplies.at( Tmp_Timeout ) ) );
	    	break;
	    }
	}
}

// ---------------------------------------------------------------- //

CommResult Receiver::processHelo( const Command &cmd )
{
    Reply reply = StandardReplies.at( Ok_Ready );
	return processBaseHelo( cmd, reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processEhlo( const Command &cmd )
{
    std::vector<std::string> esmtp;
    esmtp.push_back( "8BITMIME" );
    esmtp.push_back( "SIZE " + boost::lexical_cast <std::string>( Config::getInstance().cMaxMessageSize ) );
    esmtp.push_back( "STARTTLS" );
    //esmtp.push_back( "PIPELINING" );
    //esmtp.push_back( "CHUNKING" );
    esmtp.push_back( "AUTH PLAIN LOGIN " ); //CRAM-MD5" );

    Reply reply = StandardReplies.at( Ok_Ready );
	foreach( std::string extension, esmtp )
	{
		reply.TextLines.push_back (extension);
	}

	return processBaseHelo( cmd, reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processBaseHelo( const Command &cmd, Reply &reply )
{
    if( !isAllowedInState( cmd.Type ) )
        return sendReply( StandardReplies.at( No_BadCommandsSeq ) );

    // wyluskaj z komendy nazwe klienta
    std::vector<std::string> words;
    words = boost::split( words, cmd.Data, boost::is_any_of( Wsp ) );
    words.erase( std::remove( words.begin(), words.end(), "" ), words.end() );

    if( words.empty() )
        return sendReply( const_cast<const Reply &> ( StandardReplies.at( No_SyntaxError ) ) );

    r_cl_name = words[0];
    r_logger << info << receiver << cidIn( r_connection.cCid ) << "Klient przedstawił się jako " << r_cl_name << flush;

    r_state = HelloState;

	reply.TextLines[0] += " greets " + r_cl_name;
    return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processNoop()
{
    if( !isAllowedInState( Noop ) )
        return sendReply( StandardReplies.at( No_BadCommandsSeq ) );

    return sendReply( StandardReplies.at( Ok_Ok ) );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processQuit()
{
    if( !isAllowedInState( Quit ) )
        return sendReply( const_cast<const Reply &> ( StandardReplies.at( No_BadCommandsSeq ) ) );

    r_state = InitState;
    return sendReply( const_cast<const Reply &> ( StandardReplies.at( Ok_Closing ) ) );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processRset()
{
    if( !isAllowedInState( Rset ) )
        return sendReply( const_cast<const Reply &> ( StandardReplies.at( No_BadCommandsSeq ) ) );

    r_msg = RawMessage();
    return sendReply( const_cast<const Reply &> ( StandardReplies.at( Ok_Ok ) ) );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processUnimplemented( const Command &cmd )
{
	return sendReply( const_cast<const Reply &> ( StandardReplies.at( No_NotImplemented ) ) );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processUndefined( const Command &cmd )
{
	 return sendReply( const_cast<const Reply &> ( StandardReplies.at( No_Unrecognized ) ) );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processAuthPlain()
{
	Reply reply = StandardReplies.at( No_EncryptionRequired );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processAuthLogin()
{
	Reply reply = StandardReplies.at( No_EncryptionRequired );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processStartTLS()
{
	Reply reply = StandardReplies.at( No_NotImplemented );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processMailFrom( const Command &cmd )
{
	Reply reply = StandardReplies.at( No_AuthenticationRequired );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processRcpt( const Command &cmd )
{
	Reply reply = StandardReplies.at( No_AuthenticationRequired );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processData()
{
	Reply reply = StandardReplies.at( No_AuthenticationRequired );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::processBdat( const Command & cmd )
{
	Reply reply = StandardReplies.at( No_AuthenticationRequired );
	return sendReply( reply );
}

// ---------------------------------------------------------------- //

CommResult Receiver::recvCommand( Command &cmd, int timeout_s )
{
    // pobiera do bufora cmd.data to, co wyslal klient (max 1000 znakow)
	std::string data;

	// pobierz dane
	CommResult result = recvLine( data, timeout_s );
	if( result != Success )
		return result;

    // wydziel z nich istotne tokeny
	CommandType type =
    		boost::istarts_with( data, "mail from:" ) ? Mail
    		: boost::istarts_with( data, "rcpt to:" ) ? Rcpt
    		: boost::istarts_with( data, "helo" ) ? Helo
    		: boost::istarts_with( data, "ehlo" ) ? Ehlo
    		: boost::istarts_with( data, "data" ) ? Data
    		: boost::istarts_with( data, "rset" ) ? Rset
    		: boost::istarts_with( data, "vrfy" ) ? Vrfy
    		: boost::istarts_with( data, "expn" ) ? Expn
    		: boost::istarts_with( data, "help" ) ? Help
    		: boost::istarts_with( data, "noop" ) ? Noop
    		: boost::istarts_with( data, "quit" ) ? Quit
			: boost::istarts_with( data, "auth plain" ) ? AuthPlain
			: boost::istarts_with( data, "auth login" ) ? AuthLogin
			: boost::istarts_with( data, "starttls" ) ? StartTLS
    		: UndefinedCommand;

	cmd = Command( type, data );

    if( type != UndefinedCommand ) // odnaleziono typ, teraz tresc ( parametry )
    {
    	std::string type_name = CommandTypeToString.at (type);
    	if( type == Mail || type == Rcpt )
    		type_name += ":";

		cmd.Data = data.substr( type_name.size(), data.size() - type_name.size() );
    }

    return result;
}

// ---------------------------------------------------------------- //

CommResult Receiver::recvLine( std::string &to, int timeout_s )
{
	to.clear();

	CommResult result = r_comm->recvData( to, CRLF, timeout_s );

	result == Success
		? r_logger << debug << receiver << cidIn( r_connection.cCid ) << " Odebrano: "<< to << flush
		: r_logger << error << receiver << cidIn( r_connection.cCid ) << "Nie można odebrać danych od klienta: " << r_cl_name << flush;

    return result;
}

// ---------------------------------------------------------------- //

CommResult Receiver::recvData( std::string &to, int timeout_s )
{
	to.clear();

	CommResult result = r_comm->recvData( to, timeout_s );

	result == Success
		? r_logger << debug << receiver << cidIn( r_connection.cCid ) << " Odebrano: "<< to << flush
		: r_logger << error << receiver << cidIn( r_connection.cCid ) << "Nie można odebrać danych od klienta: " << r_cl_name << flush;

    return result;
}

// ---------------------------------------------------------------- //

CommResult Receiver::recvData( std::string &to, const std::string &until, int timeout_s )
{
	to.clear();

	CommResult result = r_comm->recvData( to, until, timeout_s );

	result == Success
		? r_logger << debug << receiver << cidIn( r_connection.cCid ) << " Odebrano: "<< to << flush
		: r_logger << error << receiver << cidIn( r_connection.cCid ) << "Nie można odebrać danych od klienta: " << r_cl_name << flush;

    return result;
}

// ---------------------------------------------------------------- //

CommResult Receiver::sendReply( const Reply &reply, int timeout_s )
{
	CommResult result = r_comm->sendData( reply.toString(), timeout_s );

	result == Success
	    ? r_logger << debug << receiver << cidIn( r_connection.cCid ) << " Wysłano: "<< reply.toString() << flush
		: r_logger << error << receiver << cidIn( r_connection.cCid ) << "Nie można wysłać danych do klienta. " << r_cl_name << flush;

    return result;
}

// ---------------------------------------------------------------- //

bool Receiver::isAllowedInState( int cmd_type )
{
    bool success = false;

    switch( r_state )
    {
        case InitState:
        case ConnectedState:
        case HelloState:
            success = cmd_type != Rcpt && cmd_type != Data
            	&& cmd_type != Bdat;
            break;
        case MailState:
            success = cmd_type == Rcpt
            	|| cmd_type == Rset || cmd_type == Noop
                || cmd_type == Help || cmd_type == Quit;
            break;
        case RcptState:
            success = cmd_type == Data || cmd_type == Bdat
            	|| cmd_type == Rcpt || cmd_type == Rset
                || cmd_type == Noop || cmd_type == Help || cmd_type == Quit;
            break;
        case DataState:
        case BdatState:
            success = cmd_type != UndefinedCommand
            	&& cmd_type != Mail && cmd_type != Rcpt
            	&& cmd_type != Data && cmd_type != Bdat
            	&& cmd_type != AuthPlain && cmd_type != AuthLogin
            	&& cmd_type != StartTLS;
            break;
        default:
            break;
    };

    return success;
}
